﻿package extremefx.modules.seaDragon {
	import extremefx.geom.Size;
	import extremefx.geom.XPoint;
	
	import flash.geom.Rectangle;		

	/**
	 * @author Marcelo Volmaro
	 */
	public class Viewport {

		private var _containerWidth:uint;
		private var _containerHeight:uint;
		
		private var _contentHeight:Number;
		private var _zoomPoint:XPoint;
		private var _homeBounds:Rectangle;
		private var _homeZoom:Number;
		private var _contentSize:Size;
		private var _centerSpringX:Spring;
		private var _zoomSpring:Spring;
		private var _centerSpringY:Spring;

		public function Viewport(pViewSize:Size, pContentSize:Size) {
			_contentSize = pContentSize;
			_contentHeight = pContentSize.height / pContentSize.width;
			
			_centerSpringX = new Spring(0);
			_centerSpringY = new Spring(0);
			_zoomSpring = new Spring(1);
			
			_zoomPoint = null;
			
			_setSize(pViewSize);
			
			goHome(true);
			update();
			_homeZoom = getZoom();
		}
		
		private function _setSize(pSize:Size):void {
			_containerWidth = pSize.width;
			_containerHeight = pSize.height;
			
			_homeBounds = new Rectangle(0, 0, 1, _contentHeight);
		}
		
		private function _getMinZoom():Number {
			var mz:Number = Math.min(_containerWidth, _containerHeight);
			var zoom:Number = (_contentSize.width <= _contentSize.height) ? mz / _containerWidth : mz / (_containerWidth * _contentHeight);
			return _homeZoom ? Math.min(zoom, _homeZoom) : mz;
		}
		
		private function _getMaxZoom():Number{
			var zoom:Number = _contentSize.width * Config.maxZoomPixelRatio / _containerWidth;
			return _homeZoom ? Math.max(zoom, _homeZoom) : Number.POSITIVE_INFINITY;
		}
		
		private function _getAspectRatio():Number {
			return _containerWidth / _containerHeight;
		}
		
		public function getBounds(pFromCurrent:Boolean = false):Rectangle {
			var center:XPoint = getCenter(pFromCurrent);
			var width:Number = 1.0 / getZoom(pFromCurrent);
			var height:Number = width / _getAspectRatio();
			
			return new Rectangle(center.x - width / 2, center.y - height / 2, width, height);
		}
		
		public function getCenter(pFromCurrent:Boolean = false):XPoint {
			var centerCurrent:XPoint = new XPoint(_centerSpringX.getCurrent(), _centerSpringY.getCurrent());
			var centerTarget:XPoint = new XPoint(_centerSpringX.getTarget(), _centerSpringY.getTarget());
			
			if (pFromCurrent) {
				return centerCurrent;
				
			} else if (!_zoomPoint) {
				return centerTarget;
			}

			var oldZoomPixel:XPoint = pixelFromPoint(_zoomPoint, true);
			
			var zoom:Number = getZoom();
			var width:Number = 1.0 / zoom;
			var height:Number = width / _getAspectRatio();
			var bounds:Rectangle = new Rectangle(centerCurrent.x - width / 2.0, centerCurrent.y - height / 2.0, width, height);
			var newZoomPixel:XPoint = _zoomPoint.minus(new XPoint(bounds.x, bounds.y)).times(_containerWidth / bounds.width);
			
			var deltaZoomPixels:XPoint = newZoomPixel.minus(oldZoomPixel);
			var deltaZoomPoints:XPoint = deltaZoomPixels.divide(_containerWidth * zoom);
			return centerTarget.plus(deltaZoomPoints);
		}
		
		public function getZoom(pFromCurrent:Boolean = false):Number {
			if (pFromCurrent) {
				return _zoomSpring.getCurrent();
				
			} else {
				return _zoomSpring.getTarget();
			}
		}
		
		public function ensureVisible(pImmediately:Boolean = false):void {
			var bounds:Rectangle = getBounds();
			var visibilityRatio:Number = Config.visibilityRatio;
			var horThres:Number = visibilityRatio * bounds.width;
			var verThres:Number = visibilityRatio * bounds.height;
			var left:Number = bounds.x + bounds.width;
			var right:Number = 1 - bounds.x;
			var top:Number = bounds.y + bounds.height;
			var bottom:Number = _contentHeight - bounds.y;
			var dx:Number = 0;
			
			if (left < horThres) {
				dx = horThres - left;
				
			} else if (right < horThres) {
				dx = right - horThres;
			}
			
			var dy:Number = 0;
			if (top < verThres) {
				dy = verThres - top;
				
			} else if (bottom < verThres) {
				dy = bottom - verThres;
			}
			
			if (dx || dy) {
				bounds.x += dx;
				bounds.y += dy;
				fitBounds(bounds, pImmediately);
			}
		}
		
		public function fitBounds(pBounds:Rectangle, pImmediately:Boolean):void {
			var aspect:Number = _getAspectRatio();
			var center:XPoint = new XPoint(pBounds.x + pBounds.width / 2, pBounds.y + pBounds.height / 2); 
			
			var newBounds:Rectangle = pBounds.clone();
			
			if ((newBounds.width / newBounds.height) >= aspect) {
				newBounds.height = pBounds.width / aspect;
				newBounds.y = center.y - newBounds.height / 2;
				
			} else {
				newBounds.width = pBounds.height * aspect;
				newBounds.x = center.x - newBounds.width / 2;
			}
			
			panTo(getCenter(true), true);
			zoomTo(getZoom(true), null, true);
			
			var zoom:Number = 1 / newBounds.width;
		
			if (zoom == getZoom()) {
				panTo(center, pImmediately);
				return;
			}
			
			var oldBounds:Rectangle = getBounds();
			var refPoint:XPoint = (new XPoint(oldBounds.x, oldBounds.y)).times(_containerWidth / oldBounds.width).minus((new XPoint(newBounds.x, newBounds.y)).times(_containerWidth / newBounds.width)).divide(_containerWidth / oldBounds.width - _containerWidth / newBounds.width);
			
			zoomTo(zoom, refPoint, pImmediately);
		}
		
		public function deltaPixelsFromPoints(pDeltaPoints:XPoint, pCurrent:Boolean = false):XPoint {
			return pDeltaPoints.times(_containerWidth * getZoom(pCurrent));
		}
		
		public function deltaPointsFromPixels(pDeltaPixels:XPoint, pCurrent:Boolean = false):XPoint {
			return pDeltaPixels.divide(_containerWidth * getZoom(pCurrent));
		}

		public function pointFromPixel(pPixels:XPoint, pCurrent:Boolean = false):XPoint {
			var bounds:Rectangle = getBounds(pCurrent);
			
			return pPixels.divide(_containerWidth / bounds.width).plus(new XPoint(bounds.x, bounds.y));
		}
		
		public function pixelFromPoint(pPoint:XPoint, pCurrent:Boolean = false):XPoint {
			var bounds:Rectangle = getBounds(pCurrent);
			return pPoint.minus(new XPoint(bounds.x, bounds.y)).times(_containerWidth / bounds.width);
		}
		
		public function update():Boolean {
			var oldCenterX:Number = _centerSpringX.getCurrent();
			var oldCenterY:Number = _centerSpringY.getCurrent();
			var oldZoom:Number = _zoomSpring.getCurrent();
			
			if (_zoomPoint) {
				var oldZoomPixel:XPoint = pixelFromPoint(_zoomPoint, true);
			}
			
			_zoomSpring.update();
			
			if (_zoomPoint && _zoomSpring.getCurrent() != oldZoom) {
				var newZoomPixel:XPoint = pixelFromPoint(_zoomPoint, true);
				var deltaZoomPixels:XPoint = newZoomPixel.minus(oldZoomPixel);
				var deltaZoomPoints:XPoint = deltaPointsFromPixels(deltaZoomPixels, true);
				_centerSpringX.shiftBy(deltaZoomPoints.x);
				_centerSpringY.shiftBy(deltaZoomPoints.y);
				
			} else {
				_zoomPoint = null;
			}
			
			_centerSpringX.update();
			_centerSpringY.update();
			return _centerSpringX.getCurrent() != oldCenterX || _centerSpringY.getCurrent() != oldCenterY || _zoomSpring.getCurrent() != oldZoom;
		}
		
		public function goHome(pImmediately:Boolean = false):void {
			fitBounds(_homeBounds, pImmediately);
		}
		
		public function panBy(pDelta:XPoint, pImmediately:Boolean = false): void {
			panTo((new XPoint(_centerSpringX.getTarget(), _centerSpringY.getTarget())).plus(pDelta), pImmediately);
		}
		
		public function panTo(pCenter:XPoint, pImmediately:Boolean = false):void {
			if (pImmediately) {
				_centerSpringX.resetTo(pCenter.x);
				_centerSpringY.resetTo(pCenter.y);
				
			} else {
				_centerSpringX.springTo(pCenter.x);
				_centerSpringY.springTo(pCenter.y);
			}
		}
		
		public function zoomBy(pFactor:Number, pRefPoint:XPoint = null, pImmediately:Boolean = false):void {
			zoomTo(_zoomSpring.getTarget() * pFactor, pRefPoint, pImmediately);
		}
		
		public function zoomTo(pZoom:Number, pRefPoint:XPoint = null, pImmediately:Boolean = false):void {
			pZoom = Math.max(pZoom, _getMinZoom());
			pZoom = Math.min(pZoom, _getMaxZoom());
			
			if (pImmediately) {
				_zoomSpring.resetTo(pZoom);
				
			} else {
				_zoomSpring.springTo(pZoom);
			}
			
			_zoomPoint = pRefPoint;
		}
		
//		public function resize(pNewSize:Size, pMaintain:Boolean = false):void {
		public function resize(pNewSize:Size):void {
			_setSize(pNewSize);
			fitBounds(getBounds(), true);
		}
	}
}
